require 'rubygems'
require 'smartcard'



class Card
  def initialize
    @context = Smartcard::PCSC::Context.new :system
    @card = nil
  end

  def list_readers
    return @context.readers
  end

  def set_reader reader
    @card = @context.card reader, :shared
  end

  def disconnect release_context=true
    @card.disconnect(:unpower)
    @ctx.release if release_context
  end

  def reconnect
    @card.reconnect :shared
  end

  def info
    return @card.info
  end
  
  #
  #
  #
  def send bytes
    command = bytes.pack('C*')
    puts "IFD ===> ICC"
    HexString.dump(command)
    response = @card.transmit command
    puts "IFD <=== ICC"
    HexString.dump(response)
    sw = response[-2,2].unpack('C*')
    if sw[0] == 0x61
      puts "IFD ===> ICC"
      HexString.dump([0x00, 0xC0, 0x00, 0x00, sw[1]].pack('C*'))
      response = @card.transmit [0x00, 0xC0, 0x00, 0x00, sw[1]].pack('C*')
      puts "IFD <=== ICC"
      HexString.dump(response)
      sw = response[-2, 2].unpack('C*')
    end
    return ByteString.new(response[0..-3].unpack('C*'),HEX), ByteString.new(response[-2,2].unpack('C*'),HEX)
  end
  
  #
  #
  #
  def sendApdu(cla,ins,p1,p2,*p)
    data = p.map{|x|
      if x.instance_of? ByteString
        [x.length]+ x.getBytes
      else
      x
      end
    }
    command = Array.new()
    command << cla << ins << p1 << p2 << data
    command.flatten!
    send command
  end

  #The SELECT command is used to explicitly select an applet on the logical
  #channel X. Once an applet has been successfully selected, the command
  #dispatcher directs all subsequent commands for that logical channel to
  #the selected applet
  # Command     CLA   INS   P1   P2   Lc   DATA
  #                   ’A4’ ’04’
  # CLA
  #     ’0X’  Select applet on logical channel X (X=0 to 3)
  # P2
  #     Specifies either the first applet or next one to be selected.
  #     '0x00' SELECT FIRST
  #     '0x02' SELECT NEXT
  def select dfname,first=true,cla=0x00
    sendApdu(cla, 0xA4, 0x04, (first ? 0x00 : 0x02), dfname);
  end

  # INITIALIZE UPDATE is used to:
  # – Transmit card and session data between the card and the host
  # – Generate session keys
  # – Initiate the setting-up of a Secure Channel
  # CLA   INS   P1   P2   Lc   DATA   Le
  # '80'  '50'      '00' '08'        '1C'
  # P1
  #    This parameter defines the Key Version Number which defines the key
  #    set within Issuer Security Domain used to initiate the Secure Channel key
  #    session.
  #    The range of the key set identifier is ’01’-’6F’. The value for the default
  #    key set is ’00’.
  def initialize_update host_random,key_version=0x00
    sendApdu(0x80, 0x50, key_version, 0x00, host_random);
  end

  def external_authenticate crypto,session_key_enc,session_key_mac,rnd
    bsEAApdu = ByteString.new("8482000010", HEX);
    
    bsEncCardRnd = crypto.sign(session_key_enc, Crypto.DES_MAC, rnd);
    #computer Mac
    bsEAApdu = bsEAApdu.concat(bsEncCardRnd);
    bsCMAC = crypto.sign(session_key_mac, Crypto.DES_MAC_EMV, bsEAApdu);
    bsToAuth = bsEncCardRnd.concat(bsCMAC);
    sendApdu(0x84, 0x82, 0x00, 0x00, bsToAuth);
  end

  #
  #delete performs a delete operation on the following objects
  #- Applet instance
  #- Package
  #CLA
  #   '80' Command is issued with Security Channel Level 0
  #   '84' Command is issued with Security Channel Level 1 or 3
  #P2
  #   '00' Only the object is to be deleted
  #   '80' The object and related data are to be deleted
  def delete tlv_aid,cla=0x80,p2=0x80
    sendApdu(cla, 0xE4, 0x00, p2, tlv_aid);
  end

  #
  #INSTALL for INSTALL [AND MAKE SELECTABLE] informs the card of the
  #various steps required to make an applet selectable within a card
  #    CLA INS P1 P2 Lc DATA Le
  #        'E6'  '00'       '01'
  # CLA
  #    '80' Command is issued within Secure Channel Level 0
  #    '84' Command is issued within Secure Channel Level 1 or 3
  # P1
  #    '04' One application is to be installed onto the card.
  #    '08' One application is to be made selectable.
  #    '0C' One application is to be installed onto the card and to be made selectable;
  #         this equals the combination of ’04’ and ’08’
  #
  #DATA INSTALL for INSTALL
  #     The data element information for the INSTALL for INSTALL command is
  #     structured as follows:
  #       Data element                        Length(Byte)
  #     Length of package AID                   1
  #     Package AID                             5-16
  #     Length of applet class AID              1
  #     Applet class AID                        0-16
  #     Length of applet instance AID           1
  #     Applet instance AID                     5-16
  #     Length of application privileges        1
  #     Application privileges                  1
  #     Length of install parameter field       1
  #     Install parameter field                 2-n
  #     Length of install token                 1
  #     Install token                           128
  def install_for_install data,cla=0x80,p1=0x0C
    sendApdu(cla, 0xE6, p1, 0x00, data)
  end

  #
  #
  #
  def read_record sfi,record_no
    response,sw = sendApdu(0x00, 0xB2, record_no, (sfi << 3) | 0x04,0)
    if sw.byteAt(0) == 0x6C
      response,sw = sendApdu(0x00, 0xB2, record_no, (sfi << 3) | 0x04, sw.byteAt(1))
    end
    return response,sw
  end

  #
  #
  #
  def get_processing_options dol_data
    sendApdu(0x80, 0xA8, 0x00, 0x00, dol_data)
  end

  #
  #
  #
  def internal_authenticate random
    return sendApdu(0x00, 0x88, 0x00, 0x00, random)
  end

  def generate_ac p1,cdol_data
    return sendApdu(0x80, 0xAE, p1, 0x00, cdol_data)
  end

  def issuer_external_authenticate iss_auth_data
    return sendApdu(0x00, 0x82, 0x00, 0x00, iss_auth_data)
  end
  
  def get_data p1,p2
    response,sw = sendApdu(0x80,0xCA,p1,p2,0x00)
    if sw.byteAt(0) == 0x6C
      response,sw = sendApdu(0x80, 0xCA,p1,p2,sw.byteAt(1))
    end
    return (sw == ByteString.new('9000',HEX) ?  TLVList.new(response, TLV.EMV).index(0).getValue : nil)
  end
end

